AWSネットワークリソースの設定変更を Microsoft Teamsで複数ユーザーにメンション通知できるようにしてみた

AWSネットワークリソースの設定変更を Microsoft Teamsで複数ユーザーにメンション通知できるようにしてみた

Clock Icon2024.10.03

こんにちは!AWS事業本部のおつまみです。

みなさん、セキュリティグループやルートテーブル、ネットワークACLなどネットワークリソースの変更・追加・削除を検知して、Teamsに通知してほしいなぁと思ったことはありますか?私はあります。
通知することで、不正や意図しない変更を迅速に把握し、対応することが可能になります。

以前こちらのブログにて、セキュリティグループの設定変更(変更・追加・削除)を通知する方法をお伝えしました。

https://dev.classmethod.jp/articles/sg-change-notify-teams-cloudformation/

今回上記のブログで展開したテンプレートにて、以下の追加要望を受けました。

  • 複数ユーザーをメンション通知できるようにしてほしい。
  • セキュリティグループ以外のネットワークリソースであるルートテーブルやネットワークACLの変更・追加・削除も通知してほしい。

この要望に沿って、CloudFormationテンプレートを修正したので、ご紹介します!

このような通知画面となります。

チームとチャネル___Test-Notify___Microsoft_Teams

※画像では松波以外メンションになっていませんが、チーム部屋にそのユーザーがいればメンションされるようになります。

構成図

今回構築する構成です。
前提として、CloudTrailが既に有効化されている環境で構築します。

sg-change-notify-teams-multi-user-mention-notification-2

AWSネットワークリソースを変更(追加・削除)から、Teams通知するまでの流れをセキュリティグループの変更を例に説明いたします。

  1. セキュリティグループの変更(追加・削除):ユーザーまたはAWSサービスがセキュリティグループに対して変更(追加・削除)を実施
  2. CloudTrailによるログ記録:AWS CloudTrailが、この変更をイベントとして記録
  3. EventBridgeルールのトリガー:設定されたEventBridgeルール(このテンプレートのEventsRuleリソース)が、CloudTrailのイベントを検知
  4. Lambda関数の呼び出し:EventBridgeルールのターゲットとして設定されたLambda関数(NotifyTeamsFunction)が呼び出されます。イベントデータがLambda関数に渡されます。
  5. イベントデータの処理とメッセージ作成:Lambda関数内のコードが、受け取ったイベントデータを処理します。関数は以下の処理を行います:
  • イベントの詳細(アカウントID、時間、リージョン、イベント名など)を抽出
  • 変更されたリソースのタイプとIDを特定
  • 設定されたユーザーリストからメンション用のテキストを生成
  • Teamsに送信するためのメッセージ(Adaptive Card形式)を作成
  1. TeamsのWebhookへの送信:Lambda関数が、作成したメッセージをTeamsのWebhook URLにHTTP URLにPOSTリクエストを送信
  2. Teamsでの通知表示:TeamsがWebhookからのデータを受け取り、指定されたチャンネルに通知を表示

なお前回はLambdaではなく、EventBridgeルールでのInputTransformerを利用し、通知していました。

sg-change-notify-teams-multi-user-mention-notification-1

Teams通知までの流れは以下のとおりです。

1~3は同様
4. イベントデータの変換:EventBridgeルールのInputTransformerが、検知したイベントデータを、Teamsに送信するための適切な形式に変換
5. API Destinationの呼び出し:変換されたデータが、設定されたAPI Destination(EventsApiDestinationリソース)に送
6. TeamsのWebhookへの送信:API DestinationがTeamsのWebhook URLにPOSTリクエストを送信
7. Teamsでの通知表示:TeamsがWebhookからのデータを受け取り、指定されたチャンネルに通知を表示

CloudFormationテンプレートを準備

テンプレート内容は以下の通りです。

テンプレート
AWSTemplateFormatVersion: "2010-09-09"
Description: "nwresource-notify-teams"

Parameters:
  SystemPrefix:
    Type: String
  EnvPrefix:
    Type: String
    AllowedValues:
      - prd
      - test
      - dev
      - poc
  WebhookURL:
    Type: String
  MentionedUsers:
    Type: String
    Default: "[{\"email\":\"[email protected]\",\"name\":\"User1\"},{\"email\":\"[email protected]\",\"name\":\"User2\"}]"
    Description: "JSON string of users to mention, e.g., '[{\"email\":\"[email protected]\",\"name\":\"User1\"},{\"email\":\"[email protected]\",\"name\":\"User2\"}]'"

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaExecutionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*

  NotifyTeamsFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${SystemPrefix}-${EnvPrefix}-notify-teams-function"
      RetentionInDays: 14
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain

  NotifyTeamsFunction:
    Type: AWS::Lambda::Function
    DependsOn: NotifyTeamsFunctionLogGroup
    Properties:
      FunctionName: !Sub "${SystemPrefix}-${EnvPrefix}-notify-teams-function"
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import json
          import urllib.request
          import os

          def handler(event, context):
              webhook_url = os.environ['WEBHOOK_URL']
              mentioned_users = json.loads(os.environ['MENTIONED_USERS'])

              detail = event['detail']
              account = event['account']
              time = event['time']
              region = event['region']
              event_name = detail['eventName']

              resource_id = detail.get('requestParameters', {}).get('groupId') or \
                            detail.get('responseElements', {}).get('groupId') or \
                            detail.get('requestParameters', {}).get('networkAclId') or \
                            detail.get('responseElements', {}).get('networkAcl', {}).get('networkAclId') or \
                            detail.get('requestParameters', {}).get('routeTableId') or \
                            detail.get('responseElements', {}).get('routeTable', {}).get('routeTableId') or \
                            'N/A'

              resource_type = "セキュリティグループ" if "SecurityGroup" in event_name else \
                              "ネットワークACL" if "NetworkAcl" in event_name else \
                              "ルートテーブル" if "RouteTable" in event_name else \
                              "ネットワークリソース"

              mentions = [f"\u003cat\u003e{user['name']}\u003c/at\u003e" for user in mentioned_users]
              mentions_text = ", ".join(mentions)

              message = {
                  "type": "message",
                  "attachments": [
                      {
                          "contentType": "application/vnd.microsoft.card.adaptive",
                          "content": {
                              "type": "AdaptiveCard",
                              "body": [
                                  {
                                      "type": "TextBlock",
                                      "text": f"{mentions_text}\n\n# **{resource_type}変更通知**\n\n{resource_type} : {resource_id} が変更されました。\n\n詳細:\n- アカウントID: **{account}**\n- 発生時間: **{time} (UTC)**\n- 変更ユーザー名: **{detail['userIdentity']['sessionContext']['sessionIssuer']['userName']}**\n- {resource_type}ID: **{resource_id}**\n- 検知API: **{event_name}**\n\n**詳細は以下のリンクからご確認ください。(検知してからイベント履歴確認まで15分程度時間がかかります。)**\n\n[CloudTrailイベントの詳細を確認する](https://{region}.console.aws.amazon.com/cloudtrailv2/home?region={region}#/events/{detail['eventID']})"
                                  }
                              ],
                              "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                              "version": "1.0",
                              "msteams": {
                                  "width": "full",
                                  "entities": [
                                      {
                                          "type": "mention",
                                          "text": f"\u003cat\u003e{user['name']}\u003c/at\u003e",
                                          "mentioned": {
                                              "id": user['email'],
                                              "name": user['name']
                                          }
                                      } for user in mentioned_users
                                  ]
                              }
                          }
                      }
                  ]
              }

              req = urllib.request.Request(webhook_url)
              req.add_header('Content-Type', 'application/json')
              jsondata = json.dumps(message)
              jsondataasbytes = jsondata.encode('utf-8')
              req.add_header('Content-Length', len(jsondataasbytes))
              response = urllib.request.urlopen(req, jsondataasbytes)

              return {
                  'statusCode': response.getcode(),
                  'body': response.read().decode('utf-8')
              }
      Runtime: python3.12
      Timeout: 30
      Environment:
        Variables:
          WEBHOOK_URL: !Ref WebhookURL
          MENTIONED_USERS: !Ref MentionedUsers

  EventBridgeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: InvokeLambdaFunction
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource: !GetAtt NotifyTeamsFunction.Arn

  EventsRuleSecurityGroup:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${SystemPrefix}-${EnvPrefix}-sg-notify-teams
      EventPattern:
        detail-type:
          - AWS API Call via CloudTrail
        source:
          - "aws.ec2"
        detail:
          eventSource: ["ec2.amazonaws.com"]
          eventName:
            - AuthorizeSecurityGroupIngress
            - AuthorizeSecurityGroupEgress
            - RevokeSecurityGroupIngress
            - RevokeSecurityGroupEgress
            - CreateSecurityGroup
            - DeleteSecurityGroup
            - ModifySecurityGroupRules
      State: ENABLED
      Targets:
        - Arn: !GetAtt NotifyTeamsFunction.Arn
          Id: EventsRuleSecurityGroup
      EventBusName: default

  EventsRuleNetworkACL:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${SystemPrefix}-${EnvPrefix}-nacl-notify-teams
      EventPattern:
        detail-type:
          - AWS API Call via CloudTrail
        source:
          - "aws.ec2"
        detail:
          eventSource: ["ec2.amazonaws.com"]
          eventName:
            - CreateNetworkAcl
            - DeleteNetworkAcl
            - ReplaceNetworkAclAssociation
            - CreateNetworkAclEntry
            - DeleteNetworkAclEntry
            - ReplaceNetworkAclEntry
      State: ENABLED
      Targets:
        - Arn: !GetAtt NotifyTeamsFunction.Arn
          Id: EventsRuleNetworkACL
      EventBusName: default

  EventsRuleRouteTable:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${SystemPrefix}-${EnvPrefix}-rt-notify-teams
      EventPattern:
        detail-type:
          - AWS API Call via CloudTrail
        source:
          - "aws.ec2"
        detail:
          eventSource: ["ec2.amazonaws.com"]
          eventName:
            - CreateRoute
            - DeleteRoute
            - ReplaceRoute
            - AssociateRouteTable
            - DisassociateRouteTable
            - ReplaceRouteTableAssociation
            - CreateRouteTable
            - DeleteRouteTable
            - EnableVgwRoutePropagation
            - DisableVgwRoutePropagation
            - EnableTransitGatewayRouteTablePropagation
            - DisableTransitGatewayRouteTablePropagation             
      State: ENABLED
      Targets:
        - Arn: !GetAtt NotifyTeamsFunction.Arn
          Id: EventsRuleRouteTable
      EventBusName: default

  LambdaPermissionSecurityGroup:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref NotifyTeamsFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventsRuleSecurityGroup.Arn

  LambdaPermissionNetworkACL:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref NotifyTeamsFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventsRuleNetworkACL.Arn

  LambdaPermissionRouteTable:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref NotifyTeamsFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventsRuleRouteTable.Arn

前回構成からの変更点

前回の構成から以下のような変更を行いました。

  1. EventBridge ルールの変更:

    • セキュリティグループだけでなく、ルートテーブルとネットワークACLの変更も検知するようにイベントパターンを拡張
  2. InputTransformer から Lambda 関数への変更:

    • 複雑な処理を可能にするため、InputTransformer の代わりに Lambda 関数を EventBridge ルールのターゲットとして設定
    • InputTransformerは、イベントデータの単純な文字列置換には適していますが、複雑な処理を行うことはできません。
    • 例えば、複数ユーザーのリストを動的に処理したり、条件に基づいて通知内容を変更したりすることは困難です。
    • InputTransformerの機能と制限については、AWSの公式ドキュメント「Amazon EventBridge 入力変換」をご参考ください。
  3. Lambda 関数の実装:

    • 複数ユーザーへのメンション通知を処理するロジックを実装
    • セキュリティグループ、ルートテーブル、ネットワークACLの変更を区別して適切なメッセージを生成するロジックを追加
  4. IAM ロールの権限拡張:

    • Lambda 関数が新たに追加されたリソース(ルートテーブル、ネットワークACL)の情報にアクセスできるよう、IAM ロールの権限を拡張
  5. Teams通知の変更:

    • 従来のInputTransformerの代わりに、LambdaをEventBridgeルールのターゲットとして指定
    • イベントデータを受け取り、必要な処理を行った後、Microsoft Teamsに通知を送信する流れを実現

いざ検証

CloudFromationでデプロイ

パラメータは、以下の値を入れます。

  • SystemPrefix
    • システム名
  • EnvPrefix
    • 環境名
  • WebhookURL
  • MentionedUsers

3分ほどでデプロイが完了します。

ネットワークリソースを変更

セキュリティグループのインバウンドルールを編集してみます。
変更して数秒後、想定通りのTeams通知が届きました!

  • セキュリティグループ変更時

チームとチャネル___Test-Notify___Microsoft_Teams

同じようにルートテーブルやネットワークACLも変更してみます。

  • ルートテーブル削除時
    35DF100C-5568-42AB-8127-FA88DB0745BC

  • ネットワークACL変更時

D64DC93B-A238-4FCB-B267-0A130E68B162

15分ほど経過後、Teamsに記載されているCloudTrailのリンクを選択します。ここからより詳細な情報が確認できますね。

イベントの詳細_-_CloudTrail

メンション先を変更してみた

Lambda関数の環境変数を変更するだけで、メンションできるユーザーを変更できます。

2717130A-8408-4750-A24E-1052CAD3E460

ユーザーを1人だけに変更してみました。

xxxx-test-notify-teams-function___関数___Lambda

同じようにセキュリティグループのインバウンドルールを編集してみます。
指定したユーザーにのみメンションされるようになりました。

チームとチャネル___Test-Notify___Microsoft_Teams

最後に

今回は、AWSネットワークリソースの設定変更を Microsoft Teamsで複数ユーザーにメンション通知できるようにする方法をご紹介しました!

AWSネットワークリソースの設定変更を通知することで、意図しない設定変更を早期に発見できることができます。
お使いのAWS環境でぜひ実装してみてください!

以上、おつまみ(@AWS11077)でした!

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.